//@version=6
indicator("Strong Transient Zones", overlay=true,
     max_boxes_count=500, max_labels_count=500, max_lines_count=200, max_bars_back=500)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 1 — INPUTS
// ═══════════════════════════════════════════════════════════════════════════════
string G_STEP = "━━ Transient Step ━━"
string G_LAD  = "━━ Tier Ladder ━━"
string G_PRS  = "━━ Pressure Index ━━"
string G_SIG  = "━━ Signals · Honest Outcomes ━━"
string G_VIS  = "━━ Neon Visuals ━━"
string G_DSH  = "━━ Dashboard ━━"

// Step --------------------------------------------------------------------------
string stepModeInput = input.string("Auto HTF", "Step Anchor",
     options=["Auto HTF", "Custom HTF", "Fixed Bars"], group=G_STEP,
     tooltip="Auto HTF: ladder re-anchors on a higher timeframe chosen from the chart TF (1m→15m, 5m→1H, 15m→4H, 1H→1D, 4H→1W, 1D→1M).\nCustom HTF: pick the anchor timeframe yourself.\nFixed Bars: re-anchor every N bars.")
string stepTfInput   = input.timeframe("240", "Custom Step Timeframe", group=G_STEP,
     tooltip="Used only in Custom HTF mode. Must be higher than the chart timeframe, otherwise Fixed Bars is used.")
int    stepBarsInput = input.int(12, "Fixed Step (bars)", minval=2, maxval=500, group=G_STEP,
     tooltip="Bars per step in Fixed Bars mode (and the fallback for an invalid HTF).")
int    maxStepsInput = input.int(60, "Steps Kept On Chart", minval=5, maxval=80, group=G_STEP,
     tooltip="Oldest steps are recycled past this count (6 blocks per step).")

// Ladder ------------------------------------------------------------------------
int    centerLenInput = input.int(20, "Center Length (EMA)", minval=5, maxval=400, group=G_LAD,
     tooltip="Adaptive center of the ladder. Frozen at the start of every step.")
int    atrLenInput    = input.int(20, "Volatility Unit (ATR length)", minval=2, maxval=200, group=G_LAD)
float  tier1MultInput = input.float(2.50, "Tier 1 Distance (×ATR)", minval=0.2, maxval=8.0, step=0.05, group=G_LAD,
     tooltip="Distance from the center to the inner edge of Tier 1.")
float  tierStepInput  = input.float(1.25, "Tier Thickness (×ATR)", minval=0.1, maxval=5.0, step=0.05, group=G_LAD,
     tooltip="Height of every tier block in ATR units.")
bool   regimeInput    = input.bool(true, "Volatility-Regime Scaling", group=G_LAD,
     tooltip="Widens the ladder when short-term ATR exceeds long-term ATR (and tightens it in calm regimes).")
bool   stepScaleInput = input.bool(true, "Auto-Scale Ladder To Step Length", group=G_LAD,
     tooltip="Multiplies the volatility unit by sqrt(step bars / 12) so a 1D step on a 1H chart gets a wider ladder than a 15m step on a 1m chart. Defaults are calibrated for a 12-bar step.")
float  ladderScaleInput = input.float(1.0, "Ladder Scale", minval=0.3, maxval=3.0, step=0.05, group=G_LAD,
     tooltip="Global multiplier on the ladder width. >1 = wider (fewer touches), <1 = tighter (more signals).")

// Pressure ----------------------------------------------------------------------
string sensInput  = input.string("5", "Sensitivity", options=["1","2","3","4","5","6","7","8","9","10"], group=G_PRS,
     tooltip="Lower = faster, more reactive. Higher = slower. Controls the persistence window and smoothing.")
float  obThInput  = input.float(70.0, "Overbought Threshold", minval=55.0, maxval=99.0, step=0.5, group=G_PRS)
float  osThInput  = input.float(30.0, "Oversold Threshold",   minval=1.0,  maxval=45.0, step=0.5, group=G_PRS)
float  extThInput = input.float(88.0, "Extreme Threshold",    minval=75.0, maxval=99.9, step=0.5, group=G_PRS,
     tooltip="Pressure ≥ this (or ≤ 100 − this) counts as EXTREME.")
float  wPersist   = input.float(0.30, "Weight · Persistence",  minval=0.0, maxval=1.0, step=0.05, group=G_PRS, inline="w1")
float  wDisplace  = input.float(0.25, "Displacement",          minval=0.0, maxval=1.0, step=0.05, group=G_PRS, inline="w1")
float  wVelocity  = input.float(0.15, "Weight · Velocity",     minval=0.0, maxval=1.0, step=0.05, group=G_PRS, inline="w2")
float  wRange     = input.float(0.15, "Range",                 minval=0.0, maxval=1.0, step=0.05, group=G_PRS, inline="w2")
float  wFlow      = input.float(0.15, "Weight · Flow (volume)", minval=0.0, maxval=1.0, step=0.05, group=G_PRS,
     tooltip="Ignored automatically on symbols without volume.")

// Signals -----------------------------------------------------------------------
float  minStrInput   = input.float(2.0, "Min Signal Strength (0-10)", minval=0.0, maxval=10.0, step=0.5, group=G_SIG,
     tooltip="A signal fires on the first close inside each tier, per side, per step. Lower this for more signals. 2.0 lets a plain Tier-1 entry through; 4.5 needs Tier 2 or a hot Tier 1.")
bool   gatePressure  = input.bool(false, "Require Pressure Beyond Threshold", group=G_SIG,
     tooltip="ON = classic behaviour: only signal when the Pressure Index is also beyond the OB/OS threshold. OFF = every tier entry can signal (strength still applies).")
int    sigCoolInput  = input.int(2, "Signal Cooldown (bars)", minval=0, maxval=100, group=G_SIG)
float  tgtUInput     = input.float(1.0, "Outcome · Target (units toward center)", minval=0.25, maxval=5.0, step=0.25, group=G_SIG, inline="ou",
     tooltip="Symmetric R-model. Target = this many ladder units from the signal close toward the step center (never beyond the center). Invalidation = the units on the right beyond the signal close. With 1.0 / 1.0 a coin flip scores 50%, so WIN% above 50 is edge.")
float  invUInput     = input.float(1.0, "Invalidation (units)", minval=0.25, maxval=5.0, step=0.25, group=G_SIG, inline="ou")
int    maxHoldInput  = input.int(48, "Outcome Max Hold (bars)", minval=5, maxval=1000, group=G_SIG,
     tooltip="A signal that hits neither level within this many bars is booked as TIMEOUT (a non-win, marked to market). It is still counted.")
bool   showOutcomes  = input.bool(true, "Mark Signal Outcomes", group=G_SIG,
     tooltip="✓ REV = reverted to the step center · ✕ EXT = extended through the next tier edge · ◦ END = timed out. Every resolved signal is marked.")
bool   showTargets   = input.bool(true, "Draw Target / Invalidation For Open Signals", group=G_SIG)

// Visuals -----------------------------------------------------------------------
string paletteInput  = input.string("Neon Violet / Green", "Palette",
     options=["Neon Violet / Green", "Neon Magenta / Cyan", "Neon Amber / Ice", "Custom"], group=G_VIS)
color  obCustomInput = input.color(color.rgb(170, 60, 255), "Custom Overbought", group=G_VIS, inline="cc")
color  osCustomInput = input.color(color.rgb(0, 255, 140),  "Oversold",   group=G_VIS, inline="cc")
bool   showZonesInput  = input.bool(true,  "Show Tier Blocks",     group=G_VIS, inline="v1")
bool   showBorderInput = input.bool(true,  "Neon Borders",   group=G_VIS, inline="v1")
bool   showCenterInput = input.bool(false, "Show Step Center",     group=G_VIS, inline="v2")
bool   showTagsInput   = input.bool(true,  "Tier Tags (live step)", group=G_VIS, inline="v2")
bool   showLabelsInput = input.bool(true,  "Signal Labels",        group=G_VIS, inline="v3")
bool   showRejInput    = input.bool(true,  "Rejection Markers",    group=G_VIS, inline="v3",
     tooltip="Marks a wick that reached Tier 2+ while the Pressure Index was extreme, with the close pulled back inside Tier 1.")
string labelSizeInput  = input.string("Small", "Label Size", options=["Tiny","Small","Normal"], group=G_VIS)
int    maxSigLblInput  = input.int(200, "Max Signal Labels", minval=20, maxval=450, group=G_VIS)

// Dashboard ---------------------------------------------------------------------
bool   dashOn    = input.bool(true, "Show Dashboard", group=G_DSH)
string dashPos   = input.string("Top Right", "Position", options=["Top Right","Middle Right","Bottom Right","Top Left","Middle Left","Bottom Left"], group=G_DSH)
string dashSizeS = input.string("Small", "Text Size", options=["Tiny","Small","Normal","Large"], group=G_DSH)
bool   showP1    = input.bool(true, "Panel I · Pressure State",        group=G_DSH)
bool   showP2    = input.bool(true, "Panel II · Transient Ladder Matrix", group=G_DSH)
bool   showP3    = input.bool(true, "Panel III · MTF Pressure",        group=G_DSH)
bool   showP4    = input.bool(true, "Panel IV · Signal Analytics",     group=G_DSH)
bool   showP5    = input.bool(true, "Panel V · Step Statistics",       group=G_DSH)
int    panelTr   = input.int(6, "Panel Transparency", minval=0, maxval=90, step=2, group=G_DSH)
float  sepH      = input.float(0.9, "Separator Gap (% height)", minval=0.1, maxval=4.0, step=0.1, group=G_DSH)
bool   monoFont  = input.bool(true, "Monospace Numbers", group=G_DSH)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 2 — PALETTE (neon on black)
// ═══════════════════════════════════════════════════════════════════════════════
// Tier 1 = nearest price (softer tint) → Tier 3 = outermost (deep saturated neon)
color OB_T1 = switch paletteInput
    "Neon Violet / Green" => color.rgb(214, 160, 255)
    "Neon Magenta / Cyan" => color.rgb(255, 150, 220)
    "Neon Amber / Ice"    => color.rgb(255, 214, 140)
    =>                       color.new(obCustomInput, 0)
color OB_T2 = switch paletteInput
    "Neon Violet / Green" => color.rgb(176, 96, 255)
    "Neon Magenta / Cyan" => color.rgb(255, 70, 190)
    "Neon Amber / Ice"    => color.rgb(255, 170, 60)
    =>                       color.new(obCustomInput, 0)
color OB_T3 = switch paletteInput
    "Neon Violet / Green" => color.rgb(128, 32, 255)
    "Neon Magenta / Cyan" => color.rgb(255, 0, 150)
    "Neon Amber / Ice"    => color.rgb(255, 120, 0)
    =>                       color.new(obCustomInput, 0)
color OS_T1 = switch paletteInput
    "Neon Violet / Green" => color.rgb(170, 255, 196)
    "Neon Magenta / Cyan" => color.rgb(150, 245, 255)
    "Neon Amber / Ice"    => color.rgb(170, 225, 255)
    =>                       color.new(osCustomInput, 0)
color OS_T2 = switch paletteInput
    "Neon Violet / Green" => color.rgb(96, 255, 150)
    "Neon Magenta / Cyan" => color.rgb(60, 230, 255)
    "Neon Amber / Ice"    => color.rgb(90, 190, 255)
    =>                       color.new(osCustomInput, 0)
color OS_T3 = switch paletteInput
    "Neon Violet / Green" => color.rgb(0, 230, 96)
    "Neon Magenta / Cyan" => color.rgb(0, 200, 255)
    "Neon Amber / Ice"    => color.rgb(0, 140, 255)
    =>                       color.new(osCustomInput, 0)

const int FILL_T1 = 82
const int FILL_T2 = 74
const int FILL_T3 = 64
const int HOT_FILL_T1 = 52
const int HOT_FILL_T2 = 44
const int HOT_FILL_T3 = 34
const int BORDER_IDLE = 48
const int BORDER_HOT  = 0

// Dashboard theme
color TXT     = #d9dce6
color MUTED   = #7d8597
color DIM     = #4a5163
color GOLD    = #f5b942
color WHITE   = #e8e6f0
color CLEAR   = color.new(#000000, 100)
color PANEL   = color.new(#0b0e15, panelTr)
color PANEL2  = color.new(#10141e, panelTr)
color HEAD    = color.new(#181e2c, panelTr)
color COLHD   = color.new(#131826, panelTr)
color PRICEBG = color.new(#1d2333, panelTr)

f_tint(color _c, float _toWhite) =>
    float r = color.r(_c)
    float g = color.g(_c)
    float b = color.b(_c)
    color.rgb(r + (255.0 - r) * _toWhite, g + (255.0 - g) * _toWhite, b + (255.0 - b) * _toWhite)

color obT1 = paletteInput == "Custom" ? f_tint(obCustomInput, 0.45) : OB_T1
color obT2 = paletteInput == "Custom" ? f_tint(obCustomInput, 0.20) : OB_T2
color obT3 = paletteInput == "Custom" ? obCustomInput : OB_T3
color osT1 = paletteInput == "Custom" ? f_tint(osCustomInput, 0.45) : OS_T1
color osT2 = paletteInput == "Custom" ? f_tint(osCustomInput, 0.20) : OS_T2
color osT3 = paletteInput == "Custom" ? osCustomInput : OS_T3
color obAccent = obT3
color osAccent = osT3

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 3 — HELPERS
// ═══════════════════════════════════════════════════════════════════════════════
f_clamp(float _v, float _lo, float _hi) => math.max(_lo, math.min(_hi, _v))

f_autoStepTf() =>
    string p = timeframe.period
    p == "1" ? "15" : p == "2" ? "30" : p == "3" ? "30" : p == "5" ? "60" : p == "10" ? "120" : p == "15" ? "240" : p == "30" ? "240" : p == "45" ? "240" : p == "60" ? "1D" : p == "120" ? "1D" : p == "180" ? "1D" : p == "240" ? "1W" : p == "1D" ? "1M" : p == "1W" ? "1M" : ""

f_nextTf(string _tf) =>
    _tf == "1" ? "5" : _tf == "5" ? "15" : _tf == "15" ? "30" : _tf == "30" ? "60" : _tf == "60" ? "240" : _tf == "240" ? "1D" : _tf == "1D" ? "1W" : _tf == "1W" ? "1M" : ""

f_prettyTf(string _tf) =>
    _tf == "1" ? "1m" : _tf == "5" ? "5m" : _tf == "15" ? "15m" : _tf == "30" ? "30m" : _tf == "60" ? "1H" : _tf == "120" ? "2H" : _tf == "240" ? "4H" : _tf == "1D" ? "1D" : _tf == "1W" ? "1W" : _tf == "1M" ? "1M" : _tf

f_labelSize() => labelSizeInput == "Tiny" ? size.tiny : labelSizeInput == "Normal" ? size.normal : size.small

f_px(float v)  => na(v) ? "—" : str.tostring(v, format.mintick)
f_sn(float v, string fmt) => (v > 0 ? "+" : "") + str.tostring(v, fmt)
f_pct(int n, int d) => d > 0 ? str.tostring(float(n) / d * 100.0, "#") + "%" : "—"
f_frac(int n, int d) => d > 0 ? float(n) / d : 0.0
f_stars(float sc) => sc >= 8 ? "★★★★" : sc >= 6.5 ? "★★★" : sc >= 5 ? "★★" : sc >= 3 ? "★" : "·"
f_tierWord(float _s) => _s >= 7.5 ? "EXTREME" : _s >= 5.0 ? "STRONG" : "MILD"
f_bucket(float _s) => _s >= 7.5 ? 2 : _s >= 5.0 ? 1 : 0

f_gauge(float frac, int n) =>
    int k = math.max(0, math.min(n, int(math.round(frac * n))))
    str.repeat("▰", k) + str.repeat("▱", n - k)

// Bipolar pressure gauge: 0 ◀━━━━━◉━━━━━▶ 100 with a marker at the value
f_pgauge(float _v, int n) =>
    string out = ""
    if na(_v)
        out := str.repeat("─", n)
    else
        int pos = math.max(0, math.min(n - 1, int(math.round(f_clamp(_v, 0.0, 100.0) / 100.0 * (n - 1)))))
        for i = 0 to n - 1
            out += i == pos ? "◉" : i < pos ? "━" : "─"
    out

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 4 — CORE SERIES
// ═══════════════════════════════════════════════════════════════════════════════
int   windowLen = sensInput == "1" ? 10 : sensInput == "2" ? 16 : sensInput == "3" ? 24 : sensInput == "4" ? 32 : sensInput == "5" ? 42 : sensInput == "6" ? 55 : sensInput == "7" ? 72 : sensInput == "8" ? 96 : sensInput == "9" ? 130 : 180
int   smoothLen = sensInput == "1" ? 2 : sensInput == "2" ? 2 : sensInput == "3" ? 3 : sensInput == "4" ? 3 : sensInput == "5" ? 4 : sensInput == "6" ? 4 : sensInput == "7" ? 5 : sensInput == "8" ? 6 : sensInput == "9" ? 7 : 8

float atrUnit  = ta.atr(atrLenInput)
float atrFast  = ta.atr(14)
float atrSlow  = ta.atr(100)
float volRatio = (not na(atrFast) and not na(atrSlow) and atrSlow > 0.0) ? atrFast / atrSlow : 1.0
float regime   = regimeInput ? f_clamp(0.70 + (volRatio - 1.0) * 0.60, 0.70, 1.60) : 1.0
float center   = ta.ema(hlc3, centerLenInput)
float rocFast  = ta.roc(close, 5)
float rocMean  = ta.sma(rocFast, 100)
float rocStd   = ta.stdev(rocFast, 100)
float chHi     = ta.highest(high, windowLen)
float chLo     = ta.lowest(low, windowLen)
float volAvg20 = ta.sma(volume, 20)
bool  hasVol   = not na(volume) and volAvg20 > 0.0
float clv      = (high - low) > 0.0 ? (2.0 * (close - low) / (high - low) - 1.0) : 0.0
float flowNum  = math.sum(clv * nz(volume), windowLen)
float flowDen  = math.sum(nz(volume), windowLen)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 5 — PRESSURE INDEX (0 = full oversold … 100 = full overbought)
// ═══════════════════════════════════════════════════════════════════════════════
bool coreReady = bar_index >= windowLen and not na(center) and not na(atrUnit) and atrUnit > 0.0

float persistence = na
if coreReady
    float acc = 0.0
    int   cnt = 0
    int lastIdx = math.max(windowLen, 1)
    for j = 0 to lastIdx - 1
        float c = center[j]
        float a = atrUnit[j]
        if not na(c) and not na(a) and a > 0.0
            acc += f_clamp((close[j] - c) / a, -2.0, 2.0) / 2.0
            cnt += 1
    persistence := cnt > 0 ? (acc / cnt + 1.0) / 2.0 : na

float displacement = coreReady ? (f_clamp((close - center) / atrUnit, -3.0, 3.0) + 3.0) / 6.0 : na
float velocityZ    = (not na(rocStd) and rocStd > 0.0) ? (rocFast - rocMean) / rocStd : 0.0
float velocity     = (f_clamp(velocityZ, -2.5, 2.5) + 2.5) / 5.0
float rangePos     = (not na(chHi) and not na(chLo) and chHi > chLo) ? (close - chLo) / (chHi - chLo) : 0.5
float flow         = (hasVol and flowDen > 0.0) ? (f_clamp(flowNum / flowDen, -1.0, 1.0) + 1.0) / 2.0 : 0.5
float wFlowEff     = hasVol ? wFlow : 0.0
float wSum         = wPersist + wDisplace + wVelocity + wRange + wFlowEff

float pressureRaw = na
if coreReady and not na(persistence) and wSum > 0.0
    pressureRaw := 100.0 * (wPersist * persistence + wDisplace * displacement + wVelocity * velocity + wRange * rangePos + wFlowEff * flow) / wSum

float pressureEma = ta.ema(pressureRaw, smoothLen)
float pressure    = na(pressureRaw) ? na : f_clamp(pressureEma, 0.0, 100.0)

bool isOB    = not na(pressure) and pressure >= obThInput
bool isOS    = not na(pressure) and pressure <= osThInput
bool isExtOB = not na(pressure) and pressure >= extThInput
bool isExtOS = not na(pressure) and pressure <= (100.0 - extThInput)
bool wasOB   = not na(pressure[1]) and pressure[1] >= obThInput
bool wasOS   = not na(pressure[1]) and pressure[1] <= osThInput

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 6 — STEP ENGINE (frozen tier ladder per step)
// ═══════════════════════════════════════════════════════════════════════════════
string stepTfSel  = stepModeInput == "Auto HTF" ? f_autoStepTf() : stepModeInput == "Custom HTF" ? stepTfInput : ""
string stepTfSafe = stepTfSel == "" ? timeframe.period : stepTfSel
bool   tfModeOk   = stepTfSel != "" and timeframe.in_seconds(stepTfSafe) > timeframe.in_seconds(timeframe.period)
bool   tfFlip     = timeframe.change(stepTfSafe)
bool   newStep    = tfModeOk ? tfFlip : (bar_index % stepBarsInput == 0)
float  stepBarsEst = tfModeOk ? math.max(1.0, float(timeframe.in_seconds(stepTfSafe)) / float(timeframe.in_seconds(timeframe.period))) : float(stepBarsInput)
float  stepScale   = stepScaleInput ? math.sqrt(stepBarsEst / 12.0) : 1.0

var float stCenter = na
var float stUnit   = na
var float up1 = na
var float up2 = na
var float up3 = na
var float up4 = na
var float dn1 = na
var float dn2 = na
var float dn3 = na
var float dn4 = na
var int   stStart  = na

var box bUp1 = na
var box bUp2 = na
var box bUp3 = na
var box bDn1 = na
var box bDn2 = na
var box bDn3 = na
var bool hotUp1 = false
var bool hotUp2 = false
var bool hotUp3 = false
var bool hotDn1 = false
var bool hotDn2 = false
var bool hotDn3 = false
var box[] boxPool = array.new<box>()

// Per-step bookkeeping (finalised into step statistics on the next step)
var int  stepSigUp = 0          // deepest tier signalled this step, upper side
var int  stepSigDn = 0
var array<bool> stepTouch = array.new<bool>(6, false)   // 0..2 upper T1..T3 (wick), 3..5 lower
var bool stepOB = false
var bool stepOS = false
var float stepMaxUp = 0.0        // max excursion above center in units
var float stepMaxDn = 0.0

// Step statistics (completed steps only)
var int nSteps = 0
var array<int> touchCnt = array.new<int>(6, 0)
var int nStepOB = 0
var int nStepOS = 0
var int nStepBoth = 0
var int nStepQuiet = 0
var float sumExcUp = 0.0
var float sumExcDn = 0.0

f_fillCol(color _c, int _t) => color.new(_c, _t)
f_brdCol(color _c, int _t)  => showBorderInput ? color.new(_c, _t) : color.new(_c, 100)

f_newBlock(float _top, float _bot, color _c, int _fillT) =>
    box.new(bar_index, _top, bar_index, _bot,
         border_color=f_brdCol(_c, BORDER_IDLE), border_width=1, border_style=line.style_solid,
         extend=extend.none, xloc=xloc.bar_index, bgcolor=f_fillCol(_c, _fillT),
         text="", text_size=size.tiny, text_color=color.new(_c, 20), text_halign=text.align_right, text_valign=text.align_center)

f_recycle() =>
    int cap = maxStepsInput * 6
    while array.size(boxPool) > cap
        box old = array.shift(boxPool)
        box.delete(old)

bool ladderReady = not na(center) and not na(atrUnit) and atrUnit > 0.0

if newStep and ladderReady
    // Finalise the previous step's statistics
    if not na(stStart)
        nSteps += 1
        bool anyUp = false
        bool anyDn = false
        for k = 0 to 5
            if stepTouch.get(k)
                touchCnt.set(k, touchCnt.get(k) + 1)
                if k < 3
                    anyUp := true
                else
                    anyDn := true
        nStepOB   += stepOB ? 1 : 0
        nStepOS   += stepOS ? 1 : 0
        nStepBoth += anyUp and anyDn ? 1 : 0
        nStepQuiet += not anyUp and not anyDn ? 1 : 0
        sumExcUp  += stepMaxUp
        sumExcDn  += stepMaxDn

    // Freeze the ladder from data closed at this moment
    stCenter := center
    stUnit   := atrUnit * regime * stepScale * ladderScaleInput
    float t1 = tier1MultInput
    float ts = tierStepInput
    up1 := stCenter + stUnit * t1
    up2 := up1 + stUnit * ts
    up3 := up2 + stUnit * ts
    up4 := up3 + stUnit * ts
    dn1 := stCenter - stUnit * t1
    dn2 := dn1 - stUnit * ts
    dn3 := dn2 - stUnit * ts
    dn4 := dn3 - stUnit * ts
    stStart := bar_index

    if showZonesInput
        bUp1 := f_newBlock(up2, up1, obT1, FILL_T1)
        bUp2 := f_newBlock(up3, up2, obT2, FILL_T2)
        bUp3 := f_newBlock(up4, up3, obT3, FILL_T3)
        bDn1 := f_newBlock(dn1, dn2, osT1, FILL_T1)
        bDn2 := f_newBlock(dn2, dn3, osT2, FILL_T2)
        bDn3 := f_newBlock(dn3, dn4, osT3, FILL_T3)
        array.push(boxPool, bUp1)
        array.push(boxPool, bUp2)
        array.push(boxPool, bUp3)
        array.push(boxPool, bDn1)
        array.push(boxPool, bDn2)
        array.push(boxPool, bDn3)
        f_recycle()

    hotUp1 := false
    hotUp2 := false
    hotUp3 := false
    hotDn1 := false
    hotDn2 := false
    hotDn3 := false
    stepSigUp := 0
    stepSigDn := 0
    stepTouch.fill(false)
    stepOB := false
    stepOS := false
    stepMaxUp := 0.0
    stepMaxDn := 0.0

if not na(bUp1)
    box.set_right(bUp1, bar_index)
    box.set_right(bUp2, bar_index)
    box.set_right(bUp3, bar_index)
    box.set_right(bDn1, bar_index)
    box.set_right(bDn2, bar_index)
    box.set_right(bDn3, bar_index)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 7 — TIER STATE, STRENGTH, HOT RECOLOUR
// ═══════════════════════════════════════════════════════════════════════════════
bool ladderLive = not na(up1) and not na(dn1)

int tierUp = ladderLive ? (close >= up3 ? 3 : close >= up2 ? 2 : close >= up1 ? 1 : 0) : 0
int tierDn = ladderLive ? (close <= dn3 ? 3 : close <= dn2 ? 2 : close <= dn1 ? 1 : 0) : 0
int wickUp = ladderLive ? (high >= up3 ? 3 : high >= up2 ? 2 : high >= up1 ? 1 : 0) : 0
int wickDn = ladderLive ? (low  <= dn3 ? 3 : low  <= dn2 ? 2 : low  <= dn1 ? 1 : 0) : 0
int tierNet = tierUp - tierDn

// Excursion from the frozen center in ladder units
float excUp = ladderLive and stUnit > 0.0 ? math.max(0.0, (high - stCenter) / stUnit) : 0.0
float excDn = ladderLive and stUnit > 0.0 ? math.max(0.0, (stCenter - low) / stUnit) : 0.0

// Strength 0–10 · continuous (works for any tier, no threshold needed)
float volClimax = hasVol ? f_clamp(nz(volume) / volAvg20 - 1.0, 0.0, 1.0) : 0.0
float prsUp     = na(pressure) ? 0.0 : f_clamp((pressure - 50.0) / 50.0, 0.0, 1.0) * 2.0
float prsDn     = na(pressure) ? 0.0 : f_clamp((50.0 - pressure) / 50.0, 0.0, 1.0) * 2.0
float strUp     = tierUp >= 1 ? f_clamp(tierUp * 2.0 + prsUp + volClimax + f_clamp(velocityZ / 2.5, 0.0, 1.0), 0.0, 10.0) : 0.0
float strDn     = tierDn >= 1 ? f_clamp(tierDn * 2.0 + prsDn + volClimax + f_clamp(-velocityZ / 2.5, 0.0, 1.0), 0.0, 10.0) : 0.0

bool obConfirmed = isOB and tierUp >= 1
bool osConfirmed = isOS and tierDn >= 1

// Per-step wick touches / pressure flags / excursions
if ladderLive and barstate.isconfirmed
    if wickUp >= 1
        stepTouch.set(0, true)
    if wickUp >= 2
        stepTouch.set(1, true)
    if wickUp >= 3
        stepTouch.set(2, true)
    if wickDn >= 1
        stepTouch.set(3, true)
    if wickDn >= 2
        stepTouch.set(4, true)
    if wickDn >= 3
        stepTouch.set(5, true)
    stepOB := stepOB or isOB
    stepOS := stepOS or isOS
    stepMaxUp := math.max(stepMaxUp, excUp)
    stepMaxDn := math.max(stepMaxDn, excDn)

// Hot recolour: a tier lights up when price closes inside it while pressure is beyond threshold
if barstate.isconfirmed and not na(bUp1)
    if obConfirmed
        if tierUp >= 1 and not hotUp1
            box.set_bgcolor(bUp1, f_fillCol(obT1, HOT_FILL_T1))
            box.set_border_color(bUp1, f_brdCol(obT1, BORDER_HOT))
            box.set_border_width(bUp1, 2)
            hotUp1 := true
        if tierUp >= 2 and not hotUp2
            box.set_bgcolor(bUp2, f_fillCol(obT2, HOT_FILL_T2))
            box.set_border_color(bUp2, f_brdCol(obT2, BORDER_HOT))
            box.set_border_width(bUp2, 2)
            hotUp2 := true
        if tierUp >= 3 and not hotUp3
            box.set_bgcolor(bUp3, f_fillCol(obT3, HOT_FILL_T3))
            box.set_border_color(bUp3, f_brdCol(obT3, BORDER_HOT))
            box.set_border_width(bUp3, 2)
            hotUp3 := true
    if osConfirmed
        if tierDn >= 1 and not hotDn1
            box.set_bgcolor(bDn1, f_fillCol(osT1, HOT_FILL_T1))
            box.set_border_color(bDn1, f_brdCol(osT1, BORDER_HOT))
            box.set_border_width(bDn1, 2)
            hotDn1 := true
        if tierDn >= 2 and not hotDn2
            box.set_bgcolor(bDn2, f_fillCol(osT2, HOT_FILL_T2))
            box.set_border_color(bDn2, f_brdCol(osT2, BORDER_HOT))
            box.set_border_width(bDn2, 2)
            hotDn2 := true
        if tierDn >= 3 and not hotDn3
            box.set_bgcolor(bDn3, f_fillCol(osT3, HOT_FILL_T3))
            box.set_border_color(bDn3, f_brdCol(osT3, BORDER_HOT))
            box.set_border_width(bDn3, 2)
            hotDn3 := true

if not na(bUp1)
    if showTagsInput and barstate.islast
        box.set_text(bUp1, "T1 ")
        box.set_text(bUp2, "T2 ")
        box.set_text(bUp3, "T3 ")
        box.set_text(bDn1, "T1 ")
        box.set_text(bDn2, "T2 ")
        box.set_text(bDn3, "T3 ")
    else
        box.set_text(bUp1, "")
        box.set_text(bUp2, "")
        box.set_text(bUp3, "")
        box.set_text(bDn1, "")
        box.set_text(bDn2, "")
        box.set_text(bDn3, "")

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 8 — SIGNAL ENGINE · HONEST OUTCOME TRACKING
// ═══════════════════════════════════════════════════════════════════════════════
// Every signal is a mean-reversion thesis: price entered tier k of the frozen ladder.
//   target       = tgtU ladder units from the close toward the center (capped at the center)
//   invalidation = invU ladder units beyond the close
//   center-reach = did price get back to the frozen center before resolution / timeout
// Resolution starts on the bar after the signal. Same-bar target + invalidation
// is booked as a loss. Unresolved after maxHold bars = TIMEOUT (still counted).

type Sig
    int   dir        // +1 = upper ladder (expects reversion DOWN), -1 = lower ladder
    int   tier
    int   bucket
    float entry
    float target
    float invalid
    float center
    bool  ctr
    float unit
    int   ebar
    float mfe        // best excursion toward target, units
    float mae        // worst excursion against, units
    float score
    label lbl
    line  lnT
    line  lnX

var array<Sig>   sigs    = array.new<Sig>()
var array<label> sigLbls = array.new<label>()
var array<label> outLbls = array.new<label>()

// Statistics · by tier (0..2), by side (0 upper / 1 lower), by strength bucket (0 mild 1 strong 2 extreme)
var array<int>   tN   = array.new<int>(3, 0)
var array<int>   tW   = array.new<int>(3, 0)
var array<int>   tTO  = array.new<int>(3, 0)
var array<int>   tC   = array.new<int>(3, 0)   // center reached
var array<float> tBar = array.new<float>(3, 0.0)   // sum bars-to-revert on wins
var array<float> tMfe = array.new<float>(3, 0.0)
var array<float> tMae = array.new<float>(3, 0.0)
var array<int>   sN   = array.new<int>(2, 0)
var array<int>   sW   = array.new<int>(2, 0)
var array<int>   bN   = array.new<int>(3, 0)
var array<int>   bW   = array.new<int>(3, 0)
var int   totN  = 0
var int   totW  = 0
var int   totTO = 0
var int   totC  = 0
var float totU  = 0.0        // net units captured (+ toward target on win, - on loss, mark on timeout)
var float sumWinU = 0.0
var float sumLossU = 0.0
var float eqPeak = 0.0
var float maxDD  = 0.0
var int   streak = 0
var int   lastSigBar = na
var int   lastSigDir = 0
var int   lastSigTier = 0
var float lastSigSc  = na
var int   sigUpCoolBar = -100000
var int   sigDnCoolBar = -100000

// Event flags
bool sigUpNow = false
bool sigDnNow = false
bool outRevUp = false
bool outRevDn = false
bool outExtUp = false
bool outExtDn = false

// 1 · Resolve open signals (before new entries)
if sigs.size() > 0
    for i = sigs.size() - 1 to 0
        Sig s = sigs.get(i)
        if bar_index > s.ebar
            float towardT = s.dir == 1 ? (s.entry - low) / s.unit : (high - s.entry) / s.unit
            float against = s.dir == 1 ? (high - s.entry) / s.unit : (s.entry - low) / s.unit
            s.mfe := math.max(s.mfe, towardT)
            s.mae := math.max(s.mae, against)
            bool hitT = s.dir == 1 ? low <= s.target  : high >= s.target
            bool hitX = s.dir == 1 ? high >= s.invalid : low <= s.invalid
            bool hitC = s.dir == 1 ? low <= s.center  : high >= s.center
            s.ctr := s.ctr or (hitC and not hitX)
            bool done = false
            int  kind = 0            // 1 revert · 2 extend · 3 timeout
            float res = 0.0          // units
            if hitX
                done := true
                kind := 2            // pessimistic: invalidation wins any same-bar tie
                res  := -(math.abs(s.invalid - s.entry) / s.unit)
            else if hitT
                done := true
                kind := 1
                res  := math.abs(s.entry - s.target) / s.unit
            else if bar_index - s.ebar >= maxHoldInput
                done := true
                kind := 3
                res  := (s.entry - close) * s.dir / s.unit
            if done
                int k = s.tier - 1
                int sd = s.dir == 1 ? 0 : 1
                bool win = kind == 1
                tN.set(k, tN.get(k) + 1)
                sN.set(sd, sN.get(sd) + 1)
                bN.set(s.bucket, bN.get(s.bucket) + 1)
                tMfe.set(k, tMfe.get(k) + s.mfe)
                tMae.set(k, tMae.get(k) + s.mae)
                totN += 1
                totU += res
                if win
                    tW.set(k, tW.get(k) + 1)
                    sW.set(sd, sW.get(sd) + 1)
                    bW.set(s.bucket, bW.get(s.bucket) + 1)
                    tBar.set(k, tBar.get(k) + (bar_index - s.ebar))
                    totW += 1
                    sumWinU += res
                else
                    sumLossU += -math.min(res, 0.0)
                if kind == 3
                    tTO.set(k, tTO.get(k) + 1)
                    totTO += 1
                if s.ctr
                    tC.set(k, tC.get(k) + 1)
                    totC += 1
                eqPeak := math.max(eqPeak, totU)
                maxDD  := math.max(maxDD, eqPeak - totU)
                streak := win ? (streak > 0 ? streak + 1 : 1) : (streak < 0 ? streak - 1 : -1)
                if s.dir == 1
                    outRevUp := outRevUp or kind == 1
                    outExtUp := outExtUp or kind == 2
                else
                    outRevDn := outRevDn or kind == 1
                    outExtDn := outExtDn or kind == 2
                if not na(s.lnT)
                    line.delete(s.lnT)
                if not na(s.lnX)
                    line.delete(s.lnX)
                if showOutcomes
                    float exPx = kind == 1 ? s.target : kind == 2 ? s.invalid : close
                    color xc   = kind == 1 ? (s.dir == 1 ? osAccent : obAccent) : kind == 2 ? (s.dir == 1 ? obAccent : osAccent) : MUTED
                    string ek  = kind == 1 ? (s.ctr ? "✓ REV◎ " : "✓ REV ") : kind == 2 ? "✕ EXT " : "◦ END "
                    outLbls.push(label.new(bar_index, exPx, ek + f_sn(res, "#.0") + "u",
                         xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=color.new(xc, 82),
                         textcolor=xc, size=size.tiny,
                         tooltip=(s.dir == 1 ? "Upper" : "Lower") + " T" + str.tostring(s.tier) + " signal · " + (kind == 1 ? "target reached" : kind == 2 ? "invalidated" : "timed out after " + str.tostring(maxHoldInput) + " bars") + (s.ctr ? " · center reached ◎" : "") + "\nResult " + f_sn(res, "#.00") + " ladder units · " + str.tostring(bar_index - s.ebar) + " bars\nMFE " + str.tostring(s.mfe, "#.00") + "u · MAE " + str.tostring(s.mae, "#.00") + "u"))
                    while outLbls.size() > maxSigLblInput
                        label.delete(outLbls.shift())
                sigs.remove(i)
            else if showTargets and not na(s.lnT)
                line.set_x2(s.lnT, bar_index)
                line.set_x2(s.lnX, bar_index)

// 2 · New signals · first close inside each tier, per side, per step
bool gateUpOk = not gatePressure or isOB
bool gateDnOk = not gatePressure or isOS
bool canSig   = barstate.isconfirmed and ladderLive
label noLbl   = na
line  noLn    = na

if canSig and tierUp > stepSigUp and gateUpOk and strUp >= minStrInput and bar_index - sigUpCoolBar >= sigCoolInput
    stepSigUp := tierUp
    sigUpNow  := true
    float tgt = math.max(close - tgtUInput * stUnit, stCenter)
    float inv = close + invUInput * stUnit
    Sig s = Sig.new(1, tierUp, f_bucket(strUp), close, tgt, inv, stCenter, false, stUnit, bar_index, 0.0, 0.0, strUp, noLbl, noLn, noLn)
    string tip = "OVERBOUGHT · Tier " + str.tostring(tierUp) + " entry · " + f_tierWord(strUp) + "\nStrength " + str.tostring(strUp, "#.0") + "/10 " + f_stars(strUp) + "\n— Pressure " + str.tostring(pressure, "#") + " (P " + str.tostring(persistence * 100, "#") + " · D " + str.tostring(displacement * 100, "#") + " · V " + str.tostring(velocity * 100, "#") + " · R " + str.tostring(rangePos * 100, "#") + " · F " + str.tostring(flow * 100, "#") + ")\n— Volume " + (hasVol ? str.tostring(nz(volume) / volAvg20, "#.00") + "× avg" : "n/a") + "\n— Velocity z " + f_sn(velocityZ, "#.00") + "\nTarget " + f_px(tgt) + " · Invalidation " + f_px(inv) + " · Center " + f_px(stCenter)
    if showLabelsInput
        label l = label.new(bar_index, high, "OB T" + str.tostring(tierUp) + " " + f_stars(strUp) + " " + str.tostring(strUp, "#.0"),
             xloc=xloc.bar_index, yloc=yloc.price, color=color.new(tierUp == 1 ? obT1 : tierUp == 2 ? obT2 : obT3, strUp >= 7.5 ? 0 : strUp >= 5.0 ? 15 : 35),
             style=label.style_label_down, textcolor=color.black, size=f_labelSize(), tooltip=tip)
        s.lbl := l
        sigLbls.push(l)
    if showTargets
        s.lnT := line.new(bar_index, tgt, bar_index + 1, tgt, xloc=xloc.bar_index, color=color.new(osAccent, 35), style=line.style_dotted, width=1)
        s.lnX := line.new(bar_index, inv, bar_index + 1, inv, xloc=xloc.bar_index, color=color.new(obAccent, 35), style=line.style_dotted, width=1)
    sigs.push(s)
    sigUpCoolBar := bar_index
    lastSigBar := bar_index
    lastSigDir := 1
    lastSigTier := tierUp
    lastSigSc := strUp

if canSig and tierDn > stepSigDn and gateDnOk and strDn >= minStrInput and bar_index - sigDnCoolBar >= sigCoolInput
    stepSigDn := tierDn
    sigDnNow  := true
    float tgt = math.min(close + tgtUInput * stUnit, stCenter)
    float inv = close - invUInput * stUnit
    Sig s = Sig.new(-1, tierDn, f_bucket(strDn), close, tgt, inv, stCenter, false, stUnit, bar_index, 0.0, 0.0, strDn, noLbl, noLn, noLn)
    string tip = "OVERSOLD · Tier " + str.tostring(tierDn) + " entry · " + f_tierWord(strDn) + "\nStrength " + str.tostring(strDn, "#.0") + "/10 " + f_stars(strDn) + "\n— Pressure " + str.tostring(pressure, "#") + " (P " + str.tostring(persistence * 100, "#") + " · D " + str.tostring(displacement * 100, "#") + " · V " + str.tostring(velocity * 100, "#") + " · R " + str.tostring(rangePos * 100, "#") + " · F " + str.tostring(flow * 100, "#") + ")\n— Volume " + (hasVol ? str.tostring(nz(volume) / volAvg20, "#.00") + "× avg" : "n/a") + "\n— Velocity z " + f_sn(velocityZ, "#.00") + "\nTarget " + f_px(tgt) + " · Invalidation " + f_px(inv) + " · Center " + f_px(stCenter)
    if showLabelsInput
        label l = label.new(bar_index, low, "OS T" + str.tostring(tierDn) + " " + f_stars(strDn) + " " + str.tostring(strDn, "#.0"),
             xloc=xloc.bar_index, yloc=yloc.price, color=color.new(tierDn == 1 ? osT1 : tierDn == 2 ? osT2 : osT3, strDn >= 7.5 ? 0 : strDn >= 5.0 ? 15 : 35),
             style=label.style_label_up, textcolor=color.black, size=f_labelSize(), tooltip=tip)
        s.lbl := l
        sigLbls.push(l)
    if showTargets
        s.lnT := line.new(bar_index, tgt, bar_index + 1, tgt, xloc=xloc.bar_index, color=color.new(obAccent, 35), style=line.style_dotted, width=1)
        s.lnX := line.new(bar_index, inv, bar_index + 1, inv, xloc=xloc.bar_index, color=color.new(osAccent, 35), style=line.style_dotted, width=1)
    sigs.push(s)
    sigDnCoolBar := bar_index
    lastSigBar := bar_index
    lastSigDir := -1
    lastSigTier := tierDn
    lastSigSc := strDn

// Visual cap only — statistics are never touched by label recycling
while sigLbls.size() > maxSigLblInput
    label.delete(sigLbls.shift())

// Rejection markers, pressure onsets, misc flags
bool obOnset = obConfirmed and not (wasOB and tierUp[1] >= 1)
bool osOnset = osConfirmed and not (wasOS and tierDn[1] >= 1)
bool obExit  = wasOB and not isOB
bool osExit  = wasOS and not isOS
bool t3TouchUp = ladderLive and wickUp == 3 and wickUp[1] < 3
bool t3TouchDn = ladderLive and wickDn == 3 and wickDn[1] < 3
bool rejUp = ladderLive and isExtOB and wickUp >= 2 and close < up2
bool rejDn = ladderLive and isExtOS and wickDn >= 2 and close > dn2

plotshape(showRejInput and rejUp, title="Rejection ▼", style=shape.triangledown, location=location.abovebar, color=color.new(obAccent, 0), size=size.tiny)
plotshape(showRejInput and rejDn, title="Rejection ▲", style=shape.triangleup,   location=location.belowbar, color=color.new(osAccent, 0), size=size.tiny)

color centerCol = showCenterInput and not na(stCenter) ? (isOB ? color.new(obAccent, 30) : isOS ? color.new(osAccent, 30) : color.new(WHITE, 60)) : CLEAR
plot(showCenterInput ? stCenter : na, title="Step Center", color=centerCol, linewidth=1, style=plot.style_linebr)
plot(pressure, title="Pressure Index", color=CLEAR, display=display.data_window)
plot(tierNet, title="Tier (+OB / −OS)", color=CLEAR, display=display.data_window)
plot(strUp - strDn, title="Strength (+OB / −OS)", color=CLEAR, display=display.data_window)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 9 — MTF READS (non-repainting)
// ═══════════════════════════════════════════════════════════════════════════════
string tf1 = f_nextTf(timeframe.period)
string tf2 = f_nextTf(tf1)
string tf3 = f_nextTf(tf2)
string tf1Safe = tf1 == "" ? timeframe.period : tf1
string tf2Safe = tf2 == "" ? timeframe.period : tf2
string tf3Safe = tf3 == "" ? timeframe.period : tf3

[h1p, h1q, h1t] = request.security(syminfo.tickerid, tf1Safe, [pressure[1], pressure[2], tierNet[1]], lookahead=barmerge.lookahead_on)
[h2p, h2q, h2t] = request.security(syminfo.tickerid, tf2Safe, [pressure[1], pressure[2], tierNet[1]], lookahead=barmerge.lookahead_on)
[h3p, h3q, h3t] = request.security(syminfo.tickerid, tf3Safe, [pressure[1], pressure[2], tierNet[1]], lookahead=barmerge.lookahead_on)
float v1 = tf1 == "" ? na : h1p
float v2 = tf2 == "" ? na : h2p
float v3 = tf3 == "" ? na : h3p
float q1 = tf1 == "" ? na : h1q
float q2 = tf2 == "" ? na : h2q
float q3 = tf3 == "" ? na : h3q
int   t1v = tf1 == "" ? 0 : int(nz(h1t))
int   t2v = tf2 == "" ? 0 : int(nz(h2t))
int   t3v = tf3 == "" ? 0 : int(nz(h3t))

int mtfOB = (isOB ? 1 : 0) + (not na(v1) and v1 >= obThInput ? 1 : 0) + (not na(v2) and v2 >= obThInput ? 1 : 0) + (not na(v3) and v3 >= obThInput ? 1 : 0)
int mtfOS = (isOS ? 1 : 0) + (not na(v1) and v1 <= osThInput ? 1 : 0) + (not na(v2) and v2 <= osThInput ? 1 : 0) + (not na(v3) and v3 <= osThInput ? 1 : 0)
int mtfRows = 1 + (tf1 == "" ? 0 : 1) + (tf2 == "" ? 0 : 1) + (tf3 == "" ? 0 : 1)

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 10 — DASHBOARD · five stacked panels
// ═══════════════════════════════════════════════════════════════════════════════
string dSize = dashSizeS == "Tiny" ? size.tiny : dashSizeS == "Normal" ? size.normal : dashSizeS == "Large" ? size.large : size.small
string dFont = monoFont ? font.family_monospace : font.family_default
string dPos  = switch dashPos
    "Top Right"    => position.top_right
    "Middle Right" => position.middle_right
    "Bottom Right" => position.bottom_right
    "Top Left"     => position.top_left
    "Middle Left"  => position.middle_left
    => position.bottom_left

var table dash = table.new(dPos, 9, 64, bgcolor=CLEAR, frame_width=0, border_width=0)

f_c(int c, int r, string tx, color tc, color bg, string ha, string tip) =>
    table.cell(dash, c, r, tx, text_color=tc, bgcolor=bg, text_halign=ha, text_size=dSize, text_font_family=dFont, tooltip=tip)

f_strip(int r, color ac) =>
    table.cell(dash, 0, r, "", bgcolor=ac, width=0.3)

f_sep(int r) =>
    for c = 0 to 8
        table.cell(dash, c, r, "", bgcolor=CLEAR, height=sepH)
    table.merge_cells(dash, 0, r, 8, r)

f_head(int r, string title, string sub, color ac) =>
    f_strip(r, ac)
    f_c(1, r, title, TXT, HEAD, text.align_left, "")
    table.merge_cells(dash, 1, r, 5, r)
    f_c(6, r, sub, ac, HEAD, text.align_right, "")
    table.merge_cells(dash, 6, r, 8, r)

f_kv(int r, string k, string v, color vc, color ac, string tip) =>
    color bg = r % 2 == 0 ? PANEL : PANEL2
    f_strip(r, color.new(ac, 55))
    f_c(1, r, k, MUTED, bg, text.align_left, "")
    table.merge_cells(dash, 1, r, 3, r)
    f_c(4, r, v, vc, bg, text.align_right, tip)
    table.merge_cells(dash, 4, r, 8, r)

f_colhd(int r, array<string> hd, color ac) =>
    f_strip(r, color.new(ac, 55))
    for c = 1 to 8
        f_c(c, r, hd.get(c - 1), MUTED, COLHD, c == 1 ? text.align_left : text.align_right, "")

f_note(int r, string tx, color ac) =>
    f_strip(r, color.new(ac, 55))
    f_c(1, r, tx, DIM, PANEL, text.align_center, "")
    table.merge_cells(dash, 1, r, 8, r)

// Pressure component row: name · gauge · value
f_comp(int r, string nm, float v01, color ac, string tip) =>
    color bg = r % 2 == 0 ? PANEL : PANEL2
    float v = na(v01) ? 0.5 : v01
    color vc = v >= 0.70 ? obAccent : v <= 0.30 ? osAccent : TXT
    f_strip(r, color.new(ac, 55))
    f_c(1, r, nm, MUTED, bg, text.align_left, tip)
    table.merge_cells(dash, 1, r, 3, r)
    f_c(4, r, f_pgauge(v * 100.0, 12), vc, bg, text.align_left, "")
    table.merge_cells(dash, 4, r, 7, r)
    f_c(8, r, na(v01) ? "—" : str.tostring(v * 100.0, "#"), vc, bg, text.align_right, "")

// Ladder matrix row. key 0..2 upper T1..T3 · 10 center · 3..5 lower T1..T3
f_lrow(int r, int key, float lo, float hi, color ac) =>
    color  bg   = r % 2 == 0 ? PANEL : PANEL2
    bool   isC  = key == 10
    bool   up   = key < 3
    int    k    = isC ? 0 : key % 3            // tier index 0..2
    color  lc   = isC ? WHITE : up ? (k == 0 ? obT1 : k == 1 ? obT2 : obT3) : (k == 0 ? osT1 : k == 1 ? osT2 : osT3)
    string nm   = isC ? "CENTER" : "T" + str.tostring(k + 1) + (up ? " ▲" : " ▼")
    float  edge = isC ? lo : up ? lo : hi       // inner edge of the block
    int    hN   = isC ? 0 : touchCnt.get(isC ? 0 : key)
    float  hit  = f_frac(hN, nSteps)
    int    rn   = isC ? 0 : tN.get(k)
    int    rw   = isC ? 0 : tW.get(k)
    bool   tdy  = isC ? false : stepTouch.get(isC ? 0 : key)
    bool   hot  = isC ? false : up ? (k == 0 ? hotUp1 : k == 1 ? hotUp2 : hotUp3) : (k == 0 ? hotDn1 : k == 1 ? hotDn2 : hotDn3)
    f_strip(r, hot ? lc : color.new(ac, 55))
    f_c(1, r, nm + (hot ? " ●" : ""), lc, bg, text.align_left, isC ? "Frozen step center · reversion target of every signal" : "Tier block · inner edge " + f_px(edge) + " · outer edge " + f_px(up ? hi : lo))
    f_c(2, r, isC ? f_px(lo) : f_px(edge), TXT, bg, text.align_right, isC ? "" : "Inner edge")
    f_c(3, r, f_sn((edge - close) / math.max(atrUnit, syminfo.mintick), "#.00"), MUTED, bg, text.align_right, "Distance from price in ATR")
    f_c(4, r, isC ? "—" : f_pct(hN, nSteps), isC ? DIM : color.from_gradient(hit, 0.0, 0.5, MUTED, lc), bg, text.align_right, "Share of completed steps in which a wick reached this tier")
    f_c(5, r, isC ? "—" : rn > 0 ? f_pct(rw, rn) : "—", isC ? DIM : rn == 0 ? DIM : f_frac(rw, rn) >= 0.55 ? osAccent : f_frac(rw, rn) <= 0.45 ? obAccent : TXT, bg, text.align_right, "WIN% of resolved signals from this tier (both sides) in the symmetric R-model · 50% = coin flip")
    f_c(6, r, isC ? "·" : tdy ? "✓" : "·", tdy ? lc : DIM, bg, text.align_center, "Reached in the current step")
    f_c(7, r, isC ? "" : f_gauge(hit, 6), color.new(lc, 20), bg, text.align_left, "")
    table.merge_cells(dash, 7, r, 8, r)

// MTF row
f_mrow(int r, string nm, float v, float q, int tn, bool isChart, color ac) =>
    color  bg  = r % 2 == 0 ? PANEL : PANEL2
    color  vc  = na(v) ? DIM : v >= obThInput ? obAccent : v <= osThInput ? osAccent : v >= 60 ? color.new(obAccent, 35) : v <= 40 ? color.new(osAccent, 35) : TXT
    string st  = na(v) ? "—" : v >= extThInput ? "OB+" : v >= obThInput ? "OB" : v <= 100.0 - extThInput ? "OS+" : v <= osThInput ? "OS" : v >= 60 ? "▲" : v <= 40 ? "▼" : "·"
    float  dlt = na(v) or na(q) ? na : v - q
    string ds  = na(dlt) ? "—" : (dlt > 0.5 ? "▲ " : dlt < -0.5 ? "▼ " : "→ ") + f_sn(dlt, "#.0")
    string ts  = tn > 0 ? "T" + str.tostring(tn) + "▲" : tn < 0 ? "T" + str.tostring(-tn) + "▼" : "·"
    f_strip(r, color.new(ac, 55))
    f_c(1, r, (isChart ? "● " : "  ") + nm, isChart ? TXT : MUTED, bg, text.align_left, isChart ? "Chart timeframe · live" : "Last closed bar of the higher timeframe (non-repainting)")
    f_c(2, r, f_pgauge(v, 12), vc, bg, text.align_left, "")
    table.merge_cells(dash, 2, r, 5, r)
    f_c(6, r, na(v) ? "—" : str.tostring(v, "#"), vc, bg, text.align_right, "Pressure Index")
    f_c(7, r, ds, na(dlt) ? DIM : dlt > 0.5 ? color.new(obAccent, 20) : dlt < -0.5 ? color.new(osAccent, 20) : MUTED, bg, text.align_right, "Change vs the previous bar of that timeframe")
    f_c(8, r, st + " " + ts, vc, bg, text.align_right, "State · tier of that timeframe's own ladder")

// Signal analytics row
f_arow(int r, string nm, int n, int w, int c, float sumB, float sumMfe, float sumMae, color lc, color ac, string tip) =>
    color  bg = r % 2 == 0 ? PANEL : PANEL2
    float  wr = n > 0 ? float(w) / n * 100.0 : 0.0
    float  cr = n > 0 ? float(c) / n * 100.0 : 0.0
    color  wc = n == 0 ? DIM : wr >= 55 ? osAccent : wr <= 45 ? obAccent : TXT
    f_strip(r, color.new(ac, 55))
    f_c(1, r, nm, n == 0 ? DIM : lc, bg, text.align_left, tip)
    table.merge_cells(dash, 1, r, 2, r)
    f_c(3, r, str.tostring(n), n == 0 ? DIM : TXT, bg, text.align_right, "Resolved signals")
    f_c(4, r, n > 0 ? str.tostring(wr, "#.0") + "%" : "—", wc, bg, text.align_right, "Target reached before invalidation · 50% = coin flip")
    f_c(5, r, n > 0 ? str.tostring(cr, "#") + "%" : "—", n == 0 ? DIM : cr >= 50 ? osAccent : TXT, bg, text.align_right, "Share of signals that got back to the frozen step center before being invalidated / timing out")
    f_c(6, r, w > 0 ? str.tostring(sumB / w, "#.0") : "—", n == 0 ? DIM : TXT, bg, text.align_right, "Average bars to target (winning signals)")
    f_c(7, r, n > 0 ? str.tostring(sumMfe / n, "#.00") + "/" + str.tostring(sumMae / n, "#.00") : "—", n == 0 ? DIM : TXT, bg, text.align_right, "Average MFE / MAE in ladder units (ATR-scaled)")
    f_c(8, r, n > 0 ? f_gauge(wr / 100.0, 5) : "", wc, bg, text.align_left, "")

if barstate.islast
    dash.clear(0, 0, 8, 63)
    if dashOn
        int  r       = 0
        bool needSep = false

        // ── PANEL I · PRESSURE STATE ────────────────────────────────────────
        if showP1
            color ac = GOLD
            f_head(r, "◆ STZ · PRESSURE STATE", syminfo.ticker + " · " + f_prettyTf(timeframe.period), ac)
            r += 1
            string stS = na(pressure) ? "—" : isExtOB ? "◉ EXTREME OVERBOUGHT" : isOB ? "● OVERBOUGHT" : isExtOS ? "◉ EXTREME OVERSOLD" : isOS ? "● OVERSOLD" : pressure >= 60 ? "▲ LEANING OB" : pressure <= 40 ? "▼ LEANING OS" : "◆ NEUTRAL"
            f_kv(r, "Pressure Index", f_pgauge(pressure, 14) + "  " + (na(pressure) ? "—" : str.tostring(pressure, "#")) + "  " + stS, na(pressure) ? MUTED : isOB ? obAccent : isOS ? osAccent : TXT, ac, "0 = full oversold · 100 = full overbought · thresholds " + str.tostring(osThInput, "#") + " / " + str.tostring(obThInput, "#"))
            r += 1
            f_comp(r, "  Persistence", persistence, ac, "Distance-weighted time above/below the center over " + str.tostring(windowLen) + " bars")
            r += 1
            f_comp(r, "  Displacement", displacement, ac, "Close distance from the center, ±3 ATR")
            r += 1
            f_comp(r, "  Velocity", velocity, ac, "Z-scored 5-bar rate of change")
            r += 1
            f_comp(r, "  Range", rangePos, ac, "Position inside the " + str.tostring(windowLen) + "-bar high/low channel")
            r += 1
            f_comp(r, "  Flow" + (hasVol ? "" : " (no volume)"), hasVol ? flow : na, ac, "Volume-weighted close location over the window")
            r += 1
            string regS = volRatio >= 1.25 ? "EXPANDING" : volRatio <= 0.80 ? "COMPRESSED" : "NORMAL"
            f_kv(r, "Volatility Regime", regS + " · ATR14/100 " + str.tostring(volRatio, "#.00") + " · ladder ×" + str.tostring(regime, "#.00"), volRatio >= 1.25 ? obAccent : volRatio <= 0.80 ? osAccent : TXT, ac, "Short vs long ATR ratio and the resulting ladder width multiplier")
            r += 1
            string stepS = tfModeOk ? f_prettyTf(stepTfSafe) + " step" : str.tostring(stepBarsInput) + "-bar step"
            f_kv(r, "Transient Step", stepS + (na(stStart) ? "" : " · bar " + str.tostring(bar_index - stStart + 1) + " · unit " + f_px(stUnit)), TXT, ac, "Ladder re-anchors on every step. Unit = ATR × regime × step scale × ladder scale")
            r += 1
            string posS = not ladderLive ? "—" : tierUp > 0 ? "▲ INSIDE T" + str.tostring(tierUp) + " · " + f_sn((close - up1) / stUnit, "#.00") + "u above T1" : tierDn > 0 ? "▼ INSIDE T" + str.tostring(tierDn) + " · " + f_sn((close - dn1) / stUnit, "#.00") + "u below T1" : "◆ CHANNEL · " + str.tostring((close - dn1) / (up1 - dn1) * 100.0, "#") + "% of width"
            f_kv(r, "Price vs Ladder", posS, tierUp > 0 ? obAccent : tierDn > 0 ? osAccent : TXT, ac, "u = ladder units")
            r += 1
            float strNow = math.max(strUp, strDn)
            f_kv(r, "Live Strength", strNow > 0 ? str.tostring(strNow, "#.0") + "/10 " + f_stars(strNow) + " · " + f_tierWord(strNow) : "— · outside ladder", strUp > 0 ? obAccent : strDn > 0 ? osAccent : MUTED, ac, "Tier depth ×2 + pressure extremity (0-2) + volume climax (0-1) + velocity burst (0-1)")
            r += 1
            string ls = na(lastSigBar) ? "—" : (lastSigDir == 1 ? "OB T" : "OS T") + str.tostring(lastSigTier) + " " + str.tostring(lastSigSc, "#.0") + " · " + str.tostring(bar_index - lastSigBar) + " bars ago"
            f_kv(r, "Last Signal · Open", ls + " · " + str.tostring(sigs.size()) + " open", na(lastSigBar) ? MUTED : lastSigDir == 1 ? obAccent : osAccent, ac, "")
            r += 1
            needSep := true

        // ── PANEL II · TRANSIENT LADDER MATRIX ──────────────────────────────
        if showP2
            if needSep
                f_sep(r)
                r += 1
            color ac = WHITE
            f_head(r, "◆ TRANSIENT LADDER MATRIX", str.tostring(nSteps) + " steps", ac)
            r += 1
            f_colhd(r, array.from("LEVEL", "EDGE", "ATR", "HIT%", "WIN%", "STEP", "REACH", ""), ac)
            table.merge_cells(dash, 7, r, 8, r)
            r += 1
            if not ladderLive
                f_note(r, "ladder not armed yet · waiting for the first step", ac)
                r += 1
            else
                // rows top → bottom; insert the live price row at the right spot
                array<float> lo = array.from(up3, up2, up1, stCenter, dn2, dn3, dn4)
                array<float> hi = array.from(up4, up3, up2, stCenter, dn1, dn2, dn3)
                array<int>   ky = array.from(2, 1, 0, 10, 3, 4, 5)
                bool priceDone = false
                for j = 0 to 6
                    float refLvl = ky.get(j) == 10 ? stCenter : ky.get(j) < 3 ? lo.get(j) : hi.get(j)
                    if not priceDone and close >= refLvl
                        f_strip(r, ac)
                        f_c(1, r, "PRICE ◀", WHITE, PRICEBG, text.align_left, "")
                        f_c(2, r, f_px(close), WHITE, PRICEBG, text.align_right, "")
                        f_c(3, r, "ATR " + f_px(atrUnit), MUTED, PRICEBG, text.align_left, "")
                        table.merge_cells(dash, 3, r, 5, r)
                        f_c(6, r, "PRS " + (na(pressure) ? "—" : str.tostring(pressure, "#")), isOB ? obAccent : isOS ? osAccent : TXT, PRICEBG, text.align_right, "")
                        table.merge_cells(dash, 6, r, 8, r)
                        r += 1
                        priceDone := true
                    f_lrow(r, ky.get(j), lo.get(j), hi.get(j), ac)
                    r += 1
                if not priceDone
                    f_strip(r, ac)
                    f_c(1, r, "PRICE ◀", WHITE, PRICEBG, text.align_left, "")
                    f_c(2, r, f_px(close), WHITE, PRICEBG, text.align_right, "")
                    f_c(3, r, "ATR " + f_px(atrUnit), MUTED, PRICEBG, text.align_left, "")
                    table.merge_cells(dash, 3, r, 5, r)
                    f_c(6, r, "PRS " + (na(pressure) ? "—" : str.tostring(pressure, "#")), isOB ? obAccent : isOS ? osAccent : TXT, PRICEBG, text.align_right, "")
                    table.merge_cells(dash, 6, r, 8, r)
                    r += 1
            needSep := true

        // ── PANEL III · MTF PRESSURE ────────────────────────────────────────
        if showP3
            if needSep
                f_sep(r)
                r += 1
            color ac = obAccent
            f_head(r, "◆ MTF PRESSURE", "last closed HTF bar", ac)
            r += 1
            f_colhd(r, array.from("TF", "0 ◀ PRESSURE ▶ 100", "", "", "", "PRS", "Δ", "STATE"), ac)
            table.merge_cells(dash, 2, r, 5, r)
            r += 1
            f_mrow(r, f_prettyTf(timeframe.period), pressure, pressure[1], tierNet, true, ac)
            r += 1
            if tf1 != ""
                f_mrow(r, f_prettyTf(tf1), v1, q1, t1v, false, ac)
                r += 1
            if tf2 != ""
                f_mrow(r, f_prettyTf(tf2), v2, q2, t2v, false, ac)
                r += 1
            if tf3 != ""
                f_mrow(r, f_prettyTf(tf3), v3, q3, t3v, false, ac)
                r += 1
            string confS = mtfOB >= 2 ? "OB CONFLUENCE " + str.tostring(mtfOB) + "/" + str.tostring(mtfRows) + (mtfOB == mtfRows ? " · FULL STACK" : "") : mtfOS >= 2 ? "OS CONFLUENCE " + str.tostring(mtfOS) + "/" + str.tostring(mtfRows) + (mtfOS == mtfRows ? " · FULL STACK" : "") : "NO CONFLUENCE · " + str.tostring(mtfOB) + " OB / " + str.tostring(mtfOS) + " OS"
            f_kv(r, "Confluence", confS, mtfOB >= 2 ? obAccent : mtfOS >= 2 ? osAccent : MUTED, ac, "Timeframes beyond threshold, chart TF included")
            r += 1
            needSep := true

        // ── PANEL IV · SIGNAL ANALYTICS ─────────────────────────────────────
        if showP4
            if needSep
                f_sep(r)
                r += 1
            color ac = osAccent
            f_head(r, "◆ SIGNAL ANALYTICS", str.tostring(tgtUInput, "#.##") + "u / " + str.tostring(invUInput, "#.##") + "u · INV-first", ac)
            r += 1
            f_strip(r, color.new(ac, 55))
            f_c(1, r, "SIGNAL", MUTED, COLHD, text.align_left, "")
            table.merge_cells(dash, 1, r, 2, r)
            f_c(3, r, "N", MUTED, COLHD, text.align_right, "")
            f_c(4, r, "WIN%", MUTED, COLHD, text.align_right, "")
            f_c(5, r, "CTR%", MUTED, COLHD, text.align_right, "")
            f_c(6, r, "BARS", MUTED, COLHD, text.align_right, "")
            f_c(7, r, "MFE/MAE", MUTED, COLHD, text.align_right, "")
            f_c(8, r, "EDGE", MUTED, COLHD, text.align_left, "")
            r += 1
            f_arow(r, "TIER 1", tN.get(0), tW.get(0), tC.get(0), tBar.get(0), tMfe.get(0), tMae.get(0), obT1, ac, "First close inside Tier 1 (either side)")
            r += 1
            f_arow(r, "TIER 2", tN.get(1), tW.get(1), tC.get(1), tBar.get(1), tMfe.get(1), tMae.get(1), obT2, ac, "First close inside Tier 2 (either side)")
            r += 1
            f_arow(r, "TIER 3", tN.get(2), tW.get(2), tC.get(2), tBar.get(2), tMfe.get(2), tMae.get(2), obT3, ac, "First close inside Tier 3 (either side)")
            r += 1
            float allBar = tBar.get(0) + tBar.get(1) + tBar.get(2)
            float allMfe = tMfe.get(0) + tMfe.get(1) + tMfe.get(2)
            float allMae = tMae.get(0) + tMae.get(1) + tMae.get(2)
            f_arow(r, "ALL", totN, totW, totC, allBar, allMfe, allMae, TXT, ac, "All resolved signals")
            r += 1
            f_kv(r, "Exit Mix", totN > 0 ? "TARGET " + str.tostring(totW) + " · INVALIDATED " + str.tostring(totN - totW - totTO) + " · TIMEOUT " + str.tostring(totTO) : "—", TXT, ac, "Every resolved signal lands in exactly one bucket")
            r += 1
            f_kv(r, "By Side", "OB ▲ " + f_pct(sW.get(0), sN.get(0)) + " (" + str.tostring(sN.get(0)) + ") · OS ▼ " + f_pct(sW.get(1), sN.get(1)) + " (" + str.tostring(sN.get(1)) + ")", f_frac(sW.get(0), sN.get(0)) > f_frac(sW.get(1), sN.get(1)) ? obAccent : f_frac(sW.get(1), sN.get(1)) > f_frac(sW.get(0), sN.get(0)) ? osAccent : TXT, ac, "Reversion rate of upper-ladder vs lower-ladder signals")
            r += 1
            f_kv(r, "By Strength", "MILD " + f_pct(bW.get(0), bN.get(0)) + " (" + str.tostring(bN.get(0)) + ") · STRONG " + f_pct(bW.get(1), bN.get(1)) + " (" + str.tostring(bN.get(1)) + ") · EXTREME " + f_pct(bW.get(2), bN.get(2)) + " (" + str.tostring(bN.get(2)) + ")", TXT, ac, "Does a higher strength score revert more often? Check here before trusting the stars.")
            r += 1
            string pfS = sumLossU > 0 ? str.tostring(sumWinU / sumLossU, "#.00") : sumWinU > 0 ? "∞" : "—"
            f_kv(r, "Net Units · PF · Max DD", f_sn(totU, "#.0") + "u · " + pfS + " · −" + str.tostring(maxDD, "#.0") + "u", totU >= 0 ? osAccent : obAccent, ac, "Units = ladder units (ATR-scaled): +target distance on a win, −invalidation distance on a loss, mark-to-market on a timeout")
            r += 1
            f_kv(r, "Expectancy · Streak", (totN > 0 ? f_sn(totU / totN, "#.00") + "u / signal" : "—") + " · " + (streak > 0 ? "W" + str.tostring(streak) : streak < 0 ? "L" + str.tostring(-streak) : "—"), totN == 0 ? MUTED : totU >= 0 ? osAccent : obAccent, ac, "")
            r += 1
            f_note(r, "every resolved signal counted · same-bar ties booked as losses · timeouts never hidden", ac)
            r += 1
            needSep := true

        // ── PANEL V · STEP STATISTICS ───────────────────────────────────────
        if showP5
            if needSep
                f_sep(r)
                r += 1
            color ac = obT2
            f_head(r, "◆ STEP STATISTICS", "completed steps", ac)
            r += 1
            f_kv(r, "Steps Tracked", str.tostring(nSteps) + (ladderLive ? " · current live" : ""), TXT, ac, "Statistics are booked when a step completes, so the live step never inflates the rates")
            r += 1
            f_kv(r, "Tier Reach ▲", "T1 " + f_pct(touchCnt.get(0), nSteps) + " · T2 " + f_pct(touchCnt.get(1), nSteps) + " · T3 " + f_pct(touchCnt.get(2), nSteps), obAccent, ac, "Share of steps in which a wick reached each upper tier")
            r += 1
            f_kv(r, "Tier Reach ▼", "T1 " + f_pct(touchCnt.get(3), nSteps) + " · T2 " + f_pct(touchCnt.get(4), nSteps) + " · T3 " + f_pct(touchCnt.get(5), nSteps), osAccent, ac, "Share of steps in which a wick reached each lower tier")
            r += 1
            f_kv(r, "Step Character", "BOTH " + f_pct(nStepBoth, nSteps) + " · QUIET " + f_pct(nStepQuiet, nSteps) + " · OB " + f_pct(nStepOB, nSteps) + " · OS " + f_pct(nStepOS, nSteps), TXT, ac, "BOTH = both ladders touched (two-way step) · QUIET = no tier touched · OB/OS = pressure beyond threshold at some point")
            r += 1
            f_kv(r, "Avg Excursion", nSteps > 0 ? "▲ " + str.tostring(sumExcUp / nSteps, "#.00") + "u · ▼ " + str.tostring(sumExcDn / nSteps, "#.00") + "u · T1 at " + str.tostring(tier1MultInput, "#.##") + "u" : "—", TXT, ac, "Average max distance from the step center in ladder units · compare with the T1 distance to judge ladder calibration")
            r += 1
            string calS = nSteps < 20 ? "collecting…" : f_frac(touchCnt.get(0) + touchCnt.get(3), nSteps * 2) > 0.35 ? "LADDER TIGHT · raise Ladder Scale" : f_frac(touchCnt.get(0) + touchCnt.get(3), nSteps * 2) < 0.08 ? "LADDER WIDE · lower Ladder Scale" : "CALIBRATED"
            f_kv(r, "Calibration", calS, nSteps < 20 ? MUTED : str.contains(calS, "CALIBRATED") ? osAccent : GOLD, ac, "Target: ~12-25% of steps touching T1 per side")
            r += 1

// ═══════════════════════════════════════════════════════════════════════════════
// SECTION 11 — ALERTS
// ═══════════════════════════════════════════════════════════════════════════════
alertcondition(sigUpNow,                       title="STZ · Overbought Signal (any tier)", message="STZ {{ticker}} {{interval}}: overbought tier signal")
alertcondition(sigDnNow,                       title="STZ · Oversold Signal (any tier)",   message="STZ {{ticker}} {{interval}}: oversold tier signal")
alertcondition(sigUpNow and tierUp >= 2,       title="STZ · Overbought Tier 2+",  message="STZ {{ticker}} {{interval}}: overbought Tier 2+ signal")
alertcondition(sigDnNow and tierDn >= 2,       title="STZ · Oversold Tier 2+",    message="STZ {{ticker}} {{interval}}: oversold Tier 2+ signal")
alertcondition(sigUpNow and strUp >= 7.5,      title="STZ · EXTREME Overbought",  message="STZ {{ticker}} {{interval}}: EXTREME overbought signal (7.5+)")
alertcondition(sigDnNow and strDn >= 7.5,      title="STZ · EXTREME Oversold",    message="STZ {{ticker}} {{interval}}: EXTREME oversold signal (7.5+)")
alertcondition(obOnset,                        title="STZ · Pressure Overbought Onset", message="STZ {{ticker}} {{interval}}: pressure crossed into overbought inside the ladder")
alertcondition(osOnset,                        title="STZ · Pressure Oversold Onset",   message="STZ {{ticker}} {{interval}}: pressure crossed into oversold inside the ladder")
alertcondition(t3TouchUp,                      title="STZ · Tier 3 Touch (upper)", message="STZ {{ticker}} {{interval}}: wick touched upper Tier 3")
alertcondition(t3TouchDn,                      title="STZ · Tier 3 Touch (lower)", message="STZ {{ticker}} {{interval}}: wick touched lower Tier 3")
alertcondition(rejUp,                          title="STZ · Upper Rejection",    message="STZ {{ticker}} {{interval}}: upper-tier rejection wick")
alertcondition(rejDn,                          title="STZ · Lower Rejection",    message="STZ {{ticker}} {{interval}}: lower-tier rejection wick")
alertcondition(outRevUp or outRevDn,           title="STZ · Signal Target Hit",  message="STZ {{ticker}} {{interval}}: a signal reached its reversion target")
alertcondition(outExtUp or outExtDn,           title="STZ · Signal Invalidated", message="STZ {{ticker}} {{interval}}: a signal was invalidated")
alertcondition(obExit,                         title="STZ · Overbought Exit",    message="STZ {{ticker}} {{interval}}: pressure left overbought")
alertcondition(osExit,                         title="STZ · Oversold Exit",      message="STZ {{ticker}} {{interval}}: pressure left oversold")
alertcondition(mtfOB >= 3 and mtfOB[1] < 3,    title="STZ · MTF OB Confluence",  message="STZ {{ticker}} {{interval}}: overbought on 3+ timeframes")
alertcondition(mtfOS >= 3 and mtfOS[1] < 3,    title="STZ · MTF OS Confluence",  message="STZ {{ticker}} {{interval}}: oversold on 3+ timeframes")
